Repository navigation
fix(native): resolve declarations in cascade order once an element's variables are set - #474
Open
YevheniiKotyrlo wants to merge 6 commits into
Open
YevheniiKotyrlo wants to merge 6 commits into
YevheniiKotyrlo wants to merge 6 commits into
Conversation
applyDeclarations reassigned its target parameter per declaration, and a
deferred declaration's closure captured that shared binding. The closure
runs after every declaration in the rule has been walked, so it read
whatever nested target the last declaration ended on: a deferred color
beside a box-shadow checked its placeholder against the shadow entry,
missed it, and left the internal { color: true } in the style.
-webkit-line-clamp: var(--n) left { numberOfLines: true } on the prop the
same way.
Each declaration now walks into a binding of its own.
A deferred declaration resolves after every other declaration of the element, so a shorthand that resolved at runtime spread its keys over whatever a later declaration had set: border: var(--b) followed by border-color: blue painted the variable's colour, within a rule and across rules, and so did border-inline-width: var(--w) over a later border-inline-start-width. Each key belongs to the declaration that set it last (css-cascade-5 §6.4). Each element now records, per target and key, the cascade position of its last writer. A deferred shorthand learns its keys only when it resolves, so it claims them one by one and keeps the ones a later declaration holds.
This was referenced Oct 11, 2026
Contributor
Author
YevheniiKotyrlo
marked this pull request as ready for review
October 11, 2026 13:42
A shadow that omits its colour takes currentcolor, which resolves as a
deferred declaration inside the shadow. On main its placeholder reaches
the style: textShadowColor: { textShadowColor: true } for a colourless
text-shadow beside a color, and color: { color: true } on a colourless
box-shadow entry under an inherited colour.
YevheniiKotyrlo
marked this pull request as draft
October 16, 2026 14:04
A later `text-shadow` writes its offset through `textShadowOffset.width`
and `.height`, and only the leaf was claimed, so an earlier
`text-shadow: var()` that resolved late still replaced `textShadowOffset`
whole: `.earlier { --s: 2px 2px blue; text-shadow: var(--s) }` before
`.my-class { text-shadow: 1px 1px red }` drew the red shadow at the blue
offset. A declaration written through a path now claims each key it
passes through.
YevheniiKotyrlo
marked this pull request as ready for review
October 16, 2026 14:13
The two text-shadow rows are about which declaration wins, so the colour a variable carries is written in the form a runtime reader keeps whether it serialises a named colour or not.
YevheniiKotyrlo
marked this pull request as draft
October 17, 2026 01:09
…ent's variables are assigned
A declaration that reads a var() was resolved after the walk, through a
placeholder at its key, so the walk had to remember which later declaration
set each key to keep a deferred shorthand off it. A placeholder is also what
an individual transform property replaced with an empty list, so
`.a { transform: var(--t) } .b { rotate: 45deg }` lost the transform.
Every rule's variables are now assigned before any declaration resolves, and
every declaration resolves in cascade order, so a later declaration overwrites
an earlier one as the browsers apply them and the key claims are gone. A
placeholder is left only for an individual transform property, which composes
into the transform list once every rule is walked. A runtime value that
resolves to nothing still resets the property.
YevheniiKotyrlo
marked this pull request as ready for review
October 17, 2026 03:15
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Problem
A declaration whose value reads a
var()resolves after every other declaration of its element, through a placeholder left at its key. Three defects onmainfollow from that:applyDeclarations's sharedtargetbinding, so it checked its placeholder against whatever nested target the last declaration ended on, missed it, and left the placeholder in the style.color: var(--c); box-shadow: 1px 1px bluerenderscolor: { color: true },text-shadowdoes the same,-webkit-line-clamp: var(--n)beside a deferred colour rendersnumberOfLines: { numberOfLines: true }, androtate: 45degbeside abox-shadowleaves{ rotateZ: true }in the transform and writes the rotation into the shadow.border: var(--b); border-color: bluepaints the variable's colour, within a rule and across rules.border-inline-width: var(--w)does the same over a laterborder-inline-start-width, an earlier rule'stext-shadow: var(--s)replaces a later rule'stext-shadow, andanimation: var(--a)replaces a lateranimation-duration..a { transform: var(--t) } .b { rotate: 45deg }renders the rotation alone, where browsers apply both.Solution
var()substitutes the element's computed custom property (css-variables-1 §3). Then every declaration resolves in cascade order, so a later declaration overwrites an earlier one as browsers apply them.transformlist once every rule is walked. Its closure keeps its own declaration's target.main.Tests
src/__tests__/native/deferred-declarations.test.tsx:box-shadow;var()with no value, and a written value the runtime cannot read, over an earlier rule's value.src/__tests__/native/animations.test.tsx: a writtenanimation-durationafter a variableanimation, with the variable inlined and kept.main, 14 of these 22 cases fail; the 8 that pass there are the ordersmainalready gets right, kept so they stay right.Verification
On Ubuntu with the workflow's steps - build, lint, typecheck,
yarn test,node --test node/__tests__/*.mjs, both library builds, the web export, the unstaged-changes checks and commitlint - everything passes.Device evidence, before and after: #474 (comment)
Merge order
#393 and #501 build on this, so this lands first.
It shares lines with #389, #391, #411 and #488 (
native/styles/calculate-props.ts); whichever lands second rebases. With #391, a declaration avar()leaves invalid at computed-value time resolves tounset, so the second of the two to land keeps that and drops this removal for a runtime value that resolves to nothing, which leaves one the runtime cannot read ignored.Base
Branched off
main(a5002c5).